﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.Security.Cryptography;
using System.Text;
using System.Windows.Threading;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Client;
using Microsoft.Crm.Sdk.Messages;
using System.IO;
using System.Xml.Linq;
using System.Net;

namespace VisualRibbonEditor
{
	public class ConnectionInfo
	{
		public string ServerHostName { get; internal set; }
		public bool UseSsl { get; internal set; }
		public string OrganizationName { get; internal set; }
		public bool ADFSMode { get; internal set; }
		public bool CrmOnline { get; internal set; }
		public string BaseLcid { get; internal set; }
		public string DomainName { get; internal set; }
		public string UserName { get; internal set; }
		public string FullUserName { get; internal set; }
		public string EntityName { get; internal set; }
		public ConnectionInfo(Connection connection)
		{
			if (!Uri.IsWellFormedUriString(connection.ServerUrl, UriKind.Absolute)) return;
			Uri serverUri = new Uri(connection.ServerUrl);
			this.ServerHostName = serverUri.Host;
			this.ADFSMode = connection.ADFSMode;
			this.CrmOnline = connection.ServerUrl.ToLowerInvariant().Contains(".dynamics.com");
			this.UseSsl = serverUri.Scheme == "https";
			this.BaseLcid = (!String.IsNullOrEmpty(connection.BaseLanguage) ? connection.BaseLanguage : "");
			this.EntityName = connection.EntityName;
			// Organization name will be extracted from the URL when using ADFS/IFD mode
			if (connection.ADFSMode == true) //
			{
				this.OrganizationName = this.ServerHostName.Substring(0, this.ServerHostName.IndexOf(".")).ToUpper();
			} else {
				this.OrganizationName = connection.OrgName;
			}
			if (this.CrmOnline == true)
			{
				this.OrganizationName = this.ServerHostName.Substring(0, this.ServerHostName.IndexOf(".")).ToUpper();
				this.ADFSMode = false;
			}
			if (!String.IsNullOrWhiteSpace(connection.Login))
			{
				this.FullUserName = connection.Login;
				if (connection.Login.Contains(@"\")) //
				{
					//username and domain provided
					this.DomainName = connection.Login.Substring(0, connection.Login.IndexOf(@"\"));
					this.UserName = connection.Login.Substring(connection.Login.IndexOf(@"\") + 1);
				} else {
					this.DomainName = String.Empty;
					this.UserName = connection.Login;

				}
			} else {
				//no username provided
				this.DomainName = String.Empty;
				this.UserName = String.Empty;
				this.FullUserName = String.Empty;
			}
		}
	}
	public class Connection
	{
		private const string SERVICE_PATH = "xrmservices/2011/organization.svc";
		private const string FILE_PATH = "connectionInfo.xml";

		// Organization Id will be saved here during TestConnection method
		Guid _organizationId = Guid.Empty;
		public Guid OrganizationId { get { return _organizationId; } }

		// Server version
		private Version _serverVersion = new Version();
		public Version ServerVersion { get { return this._serverVersion; } }
		public bool IsCrm2011() { return this.ServerVersion.Major == 5; }
		public bool IsCrm2013() { return this.ServerVersion.Major == 6; }
		public bool IsCrm2015() { return this.ServerVersion.Major == 7; }
		public bool IsCrm2016() { return this.ServerVersion.Major == 8; }

		// Base organization language
		private string _baseLanguage = String.Empty;
		public string BaseLanguage { get { return _baseLanguage; } }

		// Returns last language used in UI, or Connection.BaseLanguage
		private string _lastLanguageUsed = String.Empty;
		public string LastLanguageUsed
		{
			get { return (String.IsNullOrEmpty(_lastLanguageUsed) ? this.BaseLanguage : _lastLanguageUsed); }
			set { if (!String.IsNullOrEmpty(value)) _lastLanguageUsed = value; }
		}

		public string ServerUrl { get; set; }
		public string OrgName { get; set; }
		public string Login { get; set; }
		public string SecurePassword { get; set; }
		public string EntityName { get; set; }
		public string Password
		{
			// if successful, returns password decrypted from SecurePassword property or String.Empty
			get
			{
				if (!String.IsNullOrEmpty(SecurePassword)) //
				{
					try
					{
						byte[] secPass = ProtectedData.Unprotect(Convert.FromBase64String(SecurePassword), null, DataProtectionScope.CurrentUser);
						string base64SecPass = Convert.ToBase64String(secPass);
						byte[] base64Pass = Convert.FromBase64String(base64SecPass);
						return Encoding.Unicode.GetString(base64Pass);
					}
					catch (Exception) { }
				}
				return String.Empty;
			}
			// encrypts password and saves it to SecurePassword property. If encryption fails, password is set to String.Empty
			set
			{
				if (String.IsNullOrEmpty(value))
				{
					SecurePassword = String.Empty;
				}
				else
				{
					try
					{
						string base64Pass = Convert.ToBase64String(Encoding.Unicode.GetBytes(value));
						byte[] secPass = ProtectedData.Protect(Convert.FromBase64String(base64Pass), null, DataProtectionScope.CurrentUser);
						SecurePassword = Convert.ToBase64String(secPass);
					}
					catch (Exception)
					{
						SecurePassword = String.Empty;
					}
				}
			}
		}

		public bool ADFSMode { get; set; }

		public bool IsConnected { get; internal set; }

		public List<Connection> Connections = new List<Connection>();

		private bool _saveConnectionInfo = false;
		private bool _savePasswords = false;

		public bool SaveConnectionInfo { get { return _saveConnectionInfo; } set { _saveConnectionInfo = value; } }
		public bool SavePasswords { get { return _savePasswords; } set { _savePasswords = value; } }

		private bool _isLoaded = false;
		public bool IsLoaded
		{
			get { return _isLoaded; }
		}

		private static Connection _currentConnection = new Connection();
		public static Connection CurrentConnection
		{
			get { return _currentConnection; }
			set { _currentConnection = value; }
		}

		public Connection()
		{
			this.IsConnected = false;
			Load();
		}
		public static Connection EmptyConnection { get { return new Connection(String.Empty, String.Empty, String.Empty, String.Empty, false, String.Empty); } }
		public Connection(string serverUrl, string orgName, string login, string password, bool adfsMode, string entityName, bool securePassword = false)
		{
			this.IsConnected = false;
			this.ServerUrl = serverUrl;
			this.OrgName = orgName;
			this.Login = login;
			this.ADFSMode = adfsMode;
			if (securePassword) this.SecurePassword = password;
			else this.Password = password;
			this.EntityName = entityName;
		}
		public Connection(Connection c)
		{
			this.IsConnected = false;
			this.ServerUrl = c.ServerUrl;
			this.OrgName = c.OrgName;
			this.Login = c.Login;
			this.SecurePassword = c.SecurePassword;
			this.ADFSMode = c.ADFSMode;
			this.EntityName = c.EntityName;
		}
		public override string ToString()
		{
			if (String.IsNullOrWhiteSpace(this.ServerUrl)) return String.Empty;

			ConnectionInfo connInfo = new ConnectionInfo(this);
			string value = String.Format("{0} on {1}", connInfo.OrganizationName, connInfo.ServerHostName);
			if (!String.IsNullOrWhiteSpace(connInfo.UserName))  value += " (" + connInfo.UserName + ")";
			return value;
		}
		public string GetId()
		{
			if (String.IsNullOrWhiteSpace(this.ServerUrl)) return String.Empty;
			ConnectionInfo cInfo = new ConnectionInfo(this);
			return cInfo.ServerHostName.ToLower() + "_" + cInfo.OrganizationName.ToLower() + "_" + cInfo.FullUserName.ToLower();
		}
		public XElement ToXml(bool savePassword = false, bool includeConnectionsList = false)
		{
			XElement c = new XElement("Connection");

			XElement serverElement = new XElement("ServerUrl");
			serverElement.SetValue(ServerUrl);
			c.Add(serverElement);

			XElement orgELement = new XElement("OrgName");
			orgELement.SetValue(OrgName);
			c.Add(orgELement);

			XElement loginElement = new XElement("Login");
			loginElement.SetValue(Login);
			c.Add(loginElement);

			XElement securePassElement = new XElement("SecurePassword");
			if (savePassword)
			{
				securePassElement.SetValue(SecurePassword);
			} else {
				securePassElement.SetValue(String.Empty);
			}
			c.Add(securePassElement);

			XElement adfsElement = new XElement("ADFSMode");
			adfsElement.SetValue(ADFSMode.ToString());
			c.Add(adfsElement);

			XElement entityNameElement = new XElement("EntityName");
			entityNameElement.SetValue(EntityName);
			c.Add(entityNameElement);

			if (includeConnectionsList)
			{
				XElement connections = new XElement("Connections");
				foreach (Connection conn in this.Connections)
				{
					connections.Add(conn.ToXml(savePassword, false));
				}
				c.Add(connections);
			}
			return c;
		}
		public void Save()
		{
			if (this.SaveConnectionInfo)
			{
				XElement c = this.ToXml(this.SavePasswords, true);
				File.WriteAllText(FILE_PATH, c.ToString());
				_isLoaded = true;
			}
			else
			{
				this.ClearSavedInfo();
			}
		}
		private static Connection FromXml(XElement connXml, bool includeConnectionsList = false)
		{
			string serverUrl = String.Empty;
			var serverXml = connXml.Element("ServerUrl");
			if (serverXml != null) serverUrl = serverXml.Value;

			string orgName = String.Empty;
			var orgXml = connXml.Element("OrgName");
			if (orgXml != null) orgName = orgXml.Value;

			string login = String.Empty;
			var loginXml = connXml.Element("Login");
			if (loginXml != null) login = loginXml.Value;

			string securePassword = String.Empty;
			var securePassXml = connXml.Element("SecurePassword");
			if (securePassXml != null) securePassword = securePassXml.Value;

			// Read Password only if SecurePassword is missing
			string password = String.Empty;
			if (String.IsNullOrEmpty(securePassword))
			{
				var passXml = connXml.Element("Password");
				if (passXml != null) password = passXml.Value;
			}
			bool adfsMode = false;
			var adfsXml = connXml.Element("ADFSMode");
			if (adfsXml != null) adfsMode = (adfsXml.Value == Boolean.TrueString);

			string entityName = String.Empty;
			var entityNameXml = connXml.Element("EntityName");
			if (entityNameXml != null) entityName = entityNameXml.Value;

			bool isPassSecure = !String.IsNullOrWhiteSpace(securePassword);
			Connection conn = new Connection(serverUrl, orgName, login, isPassSecure ? securePassword : password, adfsMode, entityName, isPassSecure);
			if (includeConnectionsList)
			{
				var conns = connXml.Element("Connections");
				if (conns == null)
				{
					conn.Connections.Add(new Connection(conn));
				} else {
					var connections = conns.Elements();
					if (connections == null)
					{
						conn.Connections.Add(new Connection(conn));
					} else {
						foreach (XElement connectionXml in connections)
						{
							conn.Connections.Add(Connection.FromXml(connectionXml));
						}
					}
				}
			}
			return conn;
		}
		private void CopyFrom(Connection connection)
		{
			this.ADFSMode = connection.ADFSMode;
			this.Login = connection.Login;
			this.OrgName = connection.OrgName;
			this.ServerUrl = connection.ServerUrl;
			this.SecurePassword = connection.SecurePassword;
			if (String.IsNullOrWhiteSpace(this.SecurePassword) && !String.IsNullOrWhiteSpace(this.Password)) this.Password = connection.Password;
			this.EntityName = connection.EntityName;
			this.Connections = connection.Connections;
		}
		private void Load()
		{
			if(File.Exists(FILE_PATH))
			{
				string data = File.ReadAllText(FILE_PATH);
				XElement c = XElement.Parse(data);
				this.CopyFrom(Connection.FromXml(c, true));
				this.SaveConnectionInfo = true;
				this.SavePasswords = !String.IsNullOrWhiteSpace(this.SecurePassword) || !String.IsNullOrWhiteSpace(this.Password);
				_isLoaded = true;
			}
		}

		public void ClearSavedInfo()
		{
			if (File.Exists(FILE_PATH)) File.Delete(FILE_PATH);
		}
		public void LoadBaseLanguage(Action<bool, string> callback)
		{
			try
			{
				Dispatcher dispatcher = Dispatcher.CurrentDispatcher;
				BackgroundWorker b = new BackgroundWorker();
				b.DoWork += (sender, e) =>
				{
					try
					{
						// Get base organization language
						Helpers.GetRecordById("organization", this.OrganizationId, new string[] { "languagecode" }, (entity) =>
						{
							if (entity.Attributes.Contains("languagecode"))
							{
								this._baseLanguage = ((int)entity.Attributes["languagecode"]).ToString();
							} else {
								dispatcher.Invoke(callback, false, "Failed to get organization base language.");
							}
						}
						);
					}
					catch (System.ServiceModel.Security.MessageSecurityException ex)
					{
						if (ex.InnerException != null) dispatcher.Invoke(callback, false, ex.InnerException.Message);
						else dispatcher.Invoke(callback, false, ex.Message);
					}
					catch (FaultException<OrganizationServiceFault> ex)
					{
						dispatcher.Invoke(callback, false, ex.Detail.Message);
					}
					catch (Exception ex)
					{
						dispatcher.Invoke(callback, false, ex.ToString());
					}
				};
				b.RunWorkerAsync();
			}
			catch (Exception ex)
			{
				callback(false, ex.Message);
			}
		}

		public void TestConnection(bool loadOrganizationId, bool loadServerVersion, Action<bool, string> callback)
		{
			try
			{
				Dispatcher dispatcher = Dispatcher.CurrentDispatcher;
				BackgroundWorker b = new BackgroundWorker();
				b.DoWork += (sender, e) =>
					{
						// Reset OrganizationId if it's to be reloaded
						if (loadOrganizationId) this._organizationId = Guid.Empty;
						// Reset ServerVersion if it's to be reloaded
						if (loadServerVersion) this._serverVersion = new Version();
						try
						{
							var svc = GetOrgService();
							// WhoAmI
							WhoAmIRequest req = new WhoAmIRequest();
							svc.ExecuteAsync(req, (resp) =>
								{
									// Load Server Version
									#region Load Server Version
									if (loadServerVersion)
									{
										RetrieveVersionRequest req2 = new RetrieveVersionRequest();
										svc.ExecuteAsync(req2, (resp2) =>
										{
											string version = ((RetrieveVersionResponse)resp2).Version;
											// Success
											if (String.IsNullOrWhiteSpace(version) || !Version.TryParse(version, out this._serverVersion))
											{
												dispatcher.Invoke(callback, false, "Invalid version number");
											}
										}
										);
									}
									#endregion Load Server Version
									// Load Organization Id
									if (loadOrganizationId) this._organizationId = ((WhoAmIResponse)resp).OrganizationId;
									// Success
									Connection.CurrentConnection.IsConnected = true;
									dispatcher.Invoke(callback, true, String.Empty);
								}
							);
						}
						catch (System.ServiceModel.Security.MessageSecurityException ex)
						{
							Connection.CurrentConnection.IsConnected = false;
							if (ex.InnerException != null) dispatcher.Invoke(callback, false, ex.InnerException.Message);
							else dispatcher.Invoke(callback, false, ex.Message);
						}
						catch (FaultException<OrganizationServiceFault> ex)
						{
							Connection.CurrentConnection.IsConnected = false;
							dispatcher.Invoke(callback, false, ex.Detail.Message);
						}
						catch(Exception ex)
						{
							Connection.CurrentConnection.IsConnected = false;
							dispatcher.Invoke(callback, false, ex.ToString());
						}

					};
				b.RunWorkerAsync();
			}
			catch (Exception ex)
			{
				callback(false, ex.Message);
			}
		}

		public string GetClientUrl()
		{
			if (!ServerUrl.EndsWith("/")) ServerUrl += "/";

			if (ServerUrl.ToLowerInvariant().Contains(".dynamics.com") || ADFSMode)
			{
				return ServerUrl;
			}
			else
			{
				return String.Concat(ServerUrl, OrgName, "/");
			}
		}

		public ServiceWrapper GetOrgService()
		{
			OrganizationServiceProxy service = null;
			int timeoutMinutes = 10;
			ClientCredentials creds = new ClientCredentials();

			if (!ServerUrl.EndsWith("/")) ServerUrl += "/";

			if (ServerUrl.ToLowerInvariant().Contains(".dynamics.com"))
			{
				//CRM Online:
				ClientCredentials deviceCredentials = Microsoft.Crm.Services.Utility.DeviceIdManager.LoadOrRegisterDevice();
				creds.UserName.UserName = this.Login;
				creds.UserName.Password = this.Password;

				Uri serviceUrl = new Uri(string.Format("{0}{1}", ServerUrl, SERVICE_PATH));
				service = new Microsoft.Xrm.Sdk.Client.OrganizationServiceProxy(serviceUrl, null, creds, deviceCredentials);
				service.ServiceConfiguration.CurrentServiceEndpoint.Behaviors.Add(new ProxyTypesBehavior());
				service.Timeout = new TimeSpan(0, timeoutMinutes, 0);
				return new ServiceWrapper(service);
			}
			else if (this.ADFSMode)
			{
				Uri serviceUrl = new Uri(string.Format("{0}{1}", ServerUrl, SERVICE_PATH));
				ClientCredentials credentials = new ClientCredentials();
				credentials.Windows.ClientCredential = CredentialCache.DefaultNetworkCredentials;
				credentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;
				if (!string.IsNullOrEmpty(Login))
				{
					credentials.UserName.UserName = this.Login;
					credentials.UserName.Password = this.Password;
				}
				service = new OrganizationServiceProxy(serviceUrl, null, credentials, null);
			}
			else
			{
				Uri serviceUrl = new Uri(string.Format("{0}{1}/{2}", ServerUrl, OrgName, SERVICE_PATH));

				if (!string.IsNullOrEmpty(Login))
				{
					if(Login.Contains(@"\")) //
					{
						//username and domain provided
						string domain = Login.Substring(0, Login.IndexOf(@"\"));
						string userName = Login.Substring(Login.IndexOf(@"\") + 1);
						creds.Windows.ClientCredential = new System.Net.NetworkCredential(userName, Password, domain);
					} else {
						//username but no domain
						creds.Windows.ClientCredential = new System.Net.NetworkCredential(Login, Password);
					}
				} else {
					//no username provided, use defualt credentials
					creds.Windows.ClientCredential = CredentialCache.DefaultNetworkCredentials;
				}
				service = new OrganizationServiceProxy(serviceUrl, null, creds, null);
			}

			service.ServiceConfiguration.CurrentServiceEndpoint.Behaviors.Add(new ProxyTypesBehavior());
			service.Timeout = new TimeSpan(0, timeoutMinutes, 0);
			return new ServiceWrapper(service);
		}
	}
}